{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "# Flask命令行接口\n",
    "\n",
    "在之前的章节 **Flask框架简介** 中我们已经使用过Flask的命令行接口。\n",
    "\n",
    "\n",
    "## Click\n",
    "\n",
    "[Click](https://github.com/pallets/click) 是 Flask 的开发团队 [Pallets](https://github.com/pallets) 的另一款开源工具，用于快速创建命令行命令。Python 内置了一个 **Argparse** 的标准库用于创建命令行，但使用起来有些繁琐。<sup><a id=\"fnr.1\" class=\"footref\" href=\"#fn.1\">1</a></sup>\n",
    "\n",
    "\n",
    "### 快速使用\n",
    "\n",
    "Click 的使用大致有两个步骤：\n",
    "\n",
    "-   使用`@click.command()`装饰一个函数，使之成为命令行接口；\n",
    "\n",
    "-   使用`@click.option()`等装饰函数，为其添加命令行选项等。\n",
    "\n",
    "它的一种典型使用形式如下：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--param', default=default_value, help='description')\n",
    "def func(param):\n",
    "    pass\n",
    "```\n",
    "\n",
    "看一下官方文档的入门例子：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--count', default=1, help='Number of greetings.')\n",
    "@click.option('--name', prompt='Your name', help='The person to greet.')\n",
    "def hello(count, name):\n",
    "    \"\"\"Simple program that greets NAME for a total of COUNT times.\"\"\"\n",
    "    for x in range(count):\n",
    "        click.echo('Hello %s!' % name)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    hello()\n",
    "```\n",
    "\n",
    "在上面的例子中，函数`hello`有两个参数：`count`和`name`，它们的值从命令行中获取。\n",
    "\n",
    "-   `@click.command()`使函数`hello`成为命令行接口；\n",
    "-   `@click.option`的第一个参数指定了命令行选项的名称，`count`的默认值是`1`，`name`的值从输入获取；\n",
    "\n",
    "-   使用`click.echo`进行输出是为了获得更好的兼容性，因为`print`在 Python2 和 Python3 的用法有些差别。\n",
    "\n",
    "\n",
    "### `click.option`\n",
    "\n",
    "`option`最基本的用法就是通过指定命令行选项的名称，从命令行读取参数值，再将其传递给函数。在上面的例子，除了设置命令行选项的名称，还会指定默认值，help 说明等，`option`常用的设置参数如下：\n",
    "\n",
    "-   `default`：设置命令行参数的默认值\n",
    "-   `help`：参数说明\n",
    "-   `type`：参数类型，可以是`string`、`int`、`float`等\n",
    "-   `prompt`：当在命令行中没有输入相应的参数时，会根据`prompt`提示用户输入\n",
    "-   `nargs`：指定命令行参数接收的值的个数\n",
    "\n",
    "\n",
    "#### 指定 `type`\n",
    "\n",
    "可以使用`type`来指定参数类型：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--rate', type=float, help='rate')\n",
    "def show(rate):\n",
    "    click.echo('rate: %s' % rate)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    show()\n",
    "```\n",
    "\n",
    "```\n",
    "$ python click_type.py --rate 1\n",
    "rate: 1.0\n",
    "$ python click_type.py --rate 0.66\n",
    "rate: 0.66\n",
    "```\n",
    "\n",
    "#### 可选值\n",
    "\n",
    "在某些情况下，一个参数的值只能是某些可选的值，如果用户输入了其他值， 应该提示用户输入正确的值。在这种情况下，可以通过`click.Choice()`来限定：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--gender', type=click.Choice(['man', 'woman']))\n",
    "def choose(gender):\n",
    "    click.echo('gender: %s' % gender)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    choose()\n",
    "```\n",
    "\n",
    "    $ python click_choice.py --gender boy\n",
    "    Usage: click_choice.py [OPTIONS]\n",
    "\n",
    "    Error: Invalid value for \"--gender\": invalid choice: boy. (choose from man, woman)\n",
    "\n",
    "    $ python click_choice.py --gender man\n",
    "    gender: man\n",
    "\n",
    "\n",
    "#### 多值参数\n",
    "\n",
    "有时，一个参数需要接收多个值。`option`支持设置固定长度的参数值，通过`nargs`指定。\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--center', nargs=2, type=float, help='center of the circle')\n",
    "@click.option('--radius', type=float, help='radius of the circle')\n",
    "def circle(center, radius):\n",
    "    click.echo('center: %s, radius: %s' % (center, radius))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    circle()\n",
    "```\n",
    "\n",
    "`option`指定了两个参数：`center`和`radius`，`center`表示二维平面上一个圆的圆心坐标，接收两个值，以元组的形式将值传递给函数，`radius`表示圆的半径。\n",
    "\n",
    "    $ python click_multi_values.py --center 3 4 --radius 10\n",
    "    center: (3.0, 4.0), radius: 10.0\n",
    "\n",
    "    $ python click_multi_values.py --center 3 4 5 --radius 10\n",
    "    Usage: click_multi_values.py [OPTIONS]\n",
    "\n",
    "    Error: Got unexpected extra argument (5)\n",
    "\n",
    "\n",
    "#### 输入密码\n",
    "\n",
    "`option`提供了两个参数来设置密码的输入：\n",
    "\n",
    "-   `hide_input`：用于隐藏输入\n",
    "-   `confirmation_promt`：用于确认输入\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True)\n",
    "def input_password(password):\n",
    "    click.echo('password: %s' % password)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    input_password()\n",
    "```\n",
    "\n",
    "    $ python click_password.py\n",
    "    Password:\n",
    "    Repeat for confirmation:\n",
    "    password: 666666\n",
    "\n",
    "Click 也提供了一种快捷的方式，通过使用`@click.password_option()`，上面的代码可以简写成：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.password_option()\n",
    "def input_password(password):\n",
    "    click.echo('password: %s' % password)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    input_password()\n",
    "```\n",
    "\n",
    "\n",
    "#### 改变命令行程序的执行\n",
    "\n",
    "有些参数会改变命令行程序的执行，比如在终端输入`python`是进入 Python 控制台， 而输入`python --version`是打印 Python 版本。Click 提供`eager`标识对参数名进行标识，如果输入该参数，则会拦截既定的命令行执行流程， 跳转去执行一个回调函数。\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "def print_version(ctx, param, value):\n",
    "    if not value or ctx.resilient_parsing:\n",
    "        return\n",
    "    click.echo('Version 1.0')\n",
    "    ctx.exit()\n",
    "\n",
    "@click.command()\n",
    "@click.option('--version', is_flag=True, callback=print_version,\n",
    "              expose_value=False, is_eager=True)\n",
    "@click.option('--name', default='Ethan', help='name')\n",
    "def hello(name):\n",
    "    click.echo('Hello %s!' % name)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    hello()\n",
    "```\n",
    "\n",
    "其中：\n",
    "\n",
    "-   `is_eager=True`表明该命令行选项优先级高于其他选项；\n",
    "-   `expose_value=False`表示如果没有输入该命令行选项，会执行既定的命令行流程；\n",
    "-   `callback`指定了输入该命令行选项时，要跳转执行的函数；\n",
    "\n",
    "    ``` \n",
    "    $ python click_eager.py\n",
    "    Hello Ethan!\n",
    "\n",
    "    $ python click_eager.py --version\n",
    "    Version 1.0\n",
    "\n",
    "    $ python click_eager.py --name Michael\n",
    "    Hello Michael!\n",
    "\n",
    "    $ python click_eager.py --version --name Ethan\n",
    "    Version 1.0\n",
    "    ```\n",
    "\n",
    "### `click.argument`\n",
    "\n",
    "除了使用`@click.option`来添加可选参数，还经常使用`@click.argument`来添加固定参数。\n",
    "\n",
    "一个简单的例子：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.argument('coordinates')\n",
    "def show(coordinates):\n",
    "    click.echo('coordinates: %s' % coordinates)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    show()\n",
    "```\n",
    "\n",
    "    $ python click_argument.py\n",
    "    Usage: click_argument.py [OPTIONS] COORDINATES\n",
    "\n",
    "    Error: Missing argument \"coordinates\".\n",
    "\n",
    "    $ python click_argument.py --help\n",
    "    Usage: click_argument.py [OPTIONS] COORDINATES\n",
    "\n",
    "    Options:\n",
    "      --help  Show this message and exit.\n",
    "\n",
    "    $ python click_argument.py --coordinates 10\n",
    "    Error: no such option: --coordinates\n",
    "\n",
    "    $ python click_argument.py 10\n",
    "    coordinates: 10\n",
    "\n",
    "\n",
    "#### 多个 `argument`\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.argument('x')\n",
    "@click.argument('y')\n",
    "@click.argument('z')\n",
    "def show(x, y, z):\n",
    "    click.echo('x: %s, y: %s, z:%s' % (x, y, z))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    show()\n",
    "```\n",
    "\n",
    "    $ python click_argument.py 10 20 30\n",
    "    x: 10, y: 20, z:30\n",
    "\n",
    "    $ python click_argument.py 10\n",
    "    Usage: click_argument.py [OPTIONS] X Y Z\n",
    "\n",
    "    Error: Missing argument \"y\".\n",
    "\n",
    "    $ python click_argument.py 10 20\n",
    "    Usage: click_argument.py [OPTIONS] X Y Z\n",
    "\n",
    "    Error: Missing argument \"z\".\n",
    "\n",
    "    $ python click_argument.py 10 20 30 40\n",
    "    Usage: click_argument.py [OPTIONS] X Y Z\n",
    "\n",
    "    Error: Got unexpected extra argument (40)\n",
    "\n",
    "\n",
    "#### 不定参数\n",
    "\n",
    "`argument`还有另外一种常见的用法，就是接收不定量的参数：\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.argument('src', nargs=-1)\n",
    "@click.argument('dst', nargs=1)\n",
    "def move(src, dst):\n",
    "    click.echo('move %s to %s' % (src, dst))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    move()\n",
    "```\n",
    "\n",
    "其中， `nargs=-1` 表明参数`src`接收不定量的参数值，参数值会以元组的形式传入函数。如果`nargs`大于等于 1，表示接收`nargs`个参数值，上面的例子中，`dst`接收一个参数值。\n",
    "\n",
    "    $ python click_argument.py file1 trash\n",
    "    move (u'file1',) to trash\n",
    "\n",
    "    $ python click_argument.py file1 file2 file3 trash\n",
    "    move (u'file1', u'file2', u'file3') to trash\n",
    "\n",
    "\n",
    "### 彩色输出\n",
    "\n",
    "使用`click.echo`进行输出，如果配合 [colorama](https://github.com/tartley/colorama) 这个模块，可以使用`click.secho`进行彩色输出。\n",
    "\n",
    "使用 pip 安装 colorama：\n",
    "\n",
    "```sh\n",
    "(flaskr_env3) $ pip install colorama\n",
    "```\n",
    "\n",
    "```python\n",
    "import click\n",
    "\n",
    "@click.command()\n",
    "@click.option('--name', help='The person to greet.')\n",
    "def hello(name):\n",
    "    click.secho('Hello %s!' % name, fg='red', underline=True)\n",
    "    click.secho('Hello %s!' % name, fg='yellow', bg='black')\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    hello()\n",
    "```\n",
    "\n",
    "-   `fg`表示前景色（即字体颜色），可选值：\n",
    "    -   `BLACK`\n",
    "    -   `RED`\n",
    "    -   `GREEN`\n",
    "    -   `YELLOW`\n",
    "    -   `BLUE`\n",
    "    -   `MAGENTA`\n",
    "    -   `CYAN`\n",
    "    -   `WHITE`\n",
    "    -   &#x2026;\n",
    "\n",
    "-   `bg`表示背景色，可选值：\n",
    "    -   `BLACK`\n",
    "    -   `RED`\n",
    "    -   `GREEN`\n",
    "    -   `YELLOW`\n",
    "    -   `BLUE`\n",
    "    -   `MAGENTA`\n",
    "    -   `CYAN`\n",
    "    -   `WHITE`\n",
    "    -   &#x2026;\n",
    "\n",
    "-   `underline`表示下划线，可选的样式：\n",
    "    -   `dim=True`\n",
    "    -   `bold=True`\n",
    "\n",
    "\n",
    "### 小结\n",
    "\n",
    "-   使用`click.command()`装饰一个函数，使其成为命令行接口。\n",
    "-   使用`click.option()`添加可选参数，支持设置固定长度的参数值。\n",
    "-   使用`click.argument()`添加固定参数，支持设置不定长度的参数值。\n",
    "\n",
    "\n",
    "## 运行Shell\n",
    "\n",
    "使用下面的shell命令来开启一个交互式的Python shell：\n",
    "\n",
    "    (flaskr_env3) $ flask shell\n",
    "\n",
    "这将开启一个交互式的Python shell，并且在其中设置好了正确的应用上下文和 本地变量（local variables）。这是通过调用应用的`Flask.make_shell_context()`方法做到的。默认地你将可访问到`app`和`g`。\n",
    "\n",
    "\n",
    "## 自定义命令\n",
    "\n",
    "Flask 使用 [click](http://click.pocoo.org/) 来实现命令行接口，这使得添加自定义命令非常容易。例如，如果你想 要一个shell 命令来初始化数据库，你可以这样做：\n",
    "\n",
    "```python\n",
    "import click\n",
    "from flask import Flask\n",
    "\n",
    "app = Flask(__name__)\n",
    "\n",
    "@app.cli.command()\n",
    "def initdb():\n",
    "    \"\"\"Initialize the database.\"\"\"\n",
    "    click.echo('Init the db')\n",
    "```\n",
    "\n",
    "在命令行中执行这个命令：\n",
    "\n",
    "```sh\n",
    "(flaskr_env3) $ flask initdb\n",
    "Init the db\n",
    "```\n",
    "\n",
    "\n",
    "## 应用上下文\n",
    "\n",
    "大多数命令需要对应用对象执行操作，所以为它们设置好应用上下文非常有必要。正因为如此，如果你在`app.cli`上通过`command()`注册了一个回调，这个回调将自动被`cli.with_appcontext()`包装（wrapped）来通知命令行系统确保设置好一个应用上下文。如果一个命令是通过`add_command`或其他方法稍后添加的，那这种行为将不可用。\n",
    "\n",
    "这种行为可以通过向装饰器传递`with_appcontext=False`来禁用：\n",
    "\n",
    "```python\n",
    "@app.cli.command(with_appcontext=False)\n",
    "def example():\n",
    "    pass\n",
    "```\n",
    "\n",
    "## 脚注\n",
    "\n",
    "<sup><a id=\"fn.1\" class=\"footnum\" href=\"#fnr.1\">1</a></sup> Click 相比于 Argparse，就好比 requests 相比于 urllib 。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "name": "6-cli-interface.ipynb"
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
